" java.vim -- Manage java imports
" Author: Ding Liu <18123715300@163.com>
" Requires: Vim-7.0 or higher
" Version: 1.0
" Licence: This program is free software; you can redistribute it and/or
"          modify it under the terms of the GNU General Public License.
"          See http://www.gnu.org/copyleft/gpl.txt
" Summary Of Features:
"   Insert import
"   Sort And Distinct imports
"   Reset Unused imports
" Usage:
"   Copy this file in your vim plugin folder
"   No classpath or configuration needed. This plugin use the regular vim search.
"   So you only need a good search index (through ctags or cscope for example)
" Commands:

if v:version < 700
    echoerr "unused-imports: this plugin requires vim >= 7."
    finish
endif

if exists("g:loaded_sortimport") || &cp
    finish
endif

if exists('g:loaded_unused_imports')
    finish
endif

let g:loaded_sortimport= 1
let g:loaded_unused_imports = '1'

if !exists('g:sortedPackage')
    let g:sortedPackage = ["java", "javax", "org", "com"]
endif

if !exists('g:packageSepDepth')
    let g:packageSepDepth = 2
endif

if !exists('g:vimporter_java_roots')
    let g:vimporter_java_roots = []
endif

if !exists('g:vimporter_add_roots')
    let g:vimporter_add_roots = ["."]
endif

if !exists('g:vimporter_fuzzy_search')
    let g:vimporter_fuzzy_search = 0
endif

if !exists('g:littlelot_java_auto_template')
    let g:littlelot_java_auto_template = 1
endif

if !exists('g:autowired_dict_s')
    let g:autowired_dict_s = {'dao': 'Dao', 'mybatis': 'MybatisDao', 'repository': 'Repository', 'mapper': 'Mapper', 'mapstruct': 'BeanMapper'}
endif

if !exists('g:autowired_dict_c')
    let g:autowired_dict_c = {'service': 'Service'}
endif

let s:fuzzy_search = 0
let s:matches_so_far = []
let s:vimporter_global_lookup = {}
let s:importPattern = '^\s*import\s\+.*;\?$'

if g:littlelot_java_auto_template == 1
  autocmd BufEnter *.java call s:LittlelotAutoGen()
endif

map <silent> <script> <Plug>JavaSortImport :set lz<CR>:call <SID>JavaSortImport()<CR>:set nolz<CR>
map <silent> <script> <Plug>Vimporter_JavaLib :call <SID>Vimporter_JavaLib(expand("<cword>"))<CR>

if !hasmapto('<Plug>JavaSortImport')
    map <unique> <Leader>is <Plug>JavaSortImport
endif
if !hasmapto('<Plug>Vimporter_JavaLib')
    map <unique> <Leader>ii <Plug>Vimporter_JavaLib
endif

fun! s:JavaSortImport()
    " save current position to set it back later
    let startLine = line(".")
    let startCol = col(".")
    call s:HighlightUnusedImports(1)

    1
    if search(s:importPattern) > 0
        let firstLine = line(".")
        normal! G
        exe "" . firstLine . "," . search(s:importPattern, 'b') . "sort u"
        if getline(".") =~ "^\s*$"
            delete
            let startLine -= 1
        endif
        1
        for name in g:sortedPackage
            let pattern = '\s*import\s\+' . name . '\..*;'
            if search(pattern) > 0
                let @a = ""
                exe 'g/' . pattern . '/d A'
                exe firstLine
                normal! "aPddG
                exe search(pattern, 'b')
                normal! j
                let firstLine = line(".")
            endif
        endfor
        1
        if (g:packageSepDepth > 0)
            while search(s:importPattern, 'W') > 0
                let curLine = getline(".")
                let curMatch = substitute(curLine, '\(^\s*import\s\+\(\.\?[^\.]\+\)\{0,' . g:packageSepDepth . '\}\).*', '\1', "")
                if (curMatch == curLine)
                    let curMatch = substitute(curMatch, '\(.*\)\..*', '\1', "")
                endif
                while match(getline("."), curMatch) >= 0
                    normal! j
                endwhile
                normal! O
            endwhile
        endif
        if getline(".") =~ "^$"
            delete
            let startLine -= 1
        endif
    endif

    if startLine > 0 && startLine >= line(".")
        " set cursor back to initial position
        call cursor(startLine, startCol)
    endif
endfun

fun! s:GetSemicolon()
    return &filetype == "kotlin" ? "" : ";"
endfun

function! s:HighlightUnusedImports(remove)
    call s:ResetUnusedHighlights()
    highlight unusedimport ctermbg=darkred guibg=darkred

    " save current position to set it back later
    let startLine = line(".")
    let startCol = col(".")

    let linenr = 0
    " where does the class definition start (= where the imports end)
    call cursor(1, 1)
    let classStartLine = search('\v(^\s*import\s+)@<!(<fun>|<class>|<object>|<interface>|\@)')
    while linenr < classStartLine
        let linenr += 1
        let line = getline(linenr)
        " assemble non static imports for removal
        let lis = matchlist(line, '\v^\s*import\s+(\w+\.)+([a-zA-Z0-9_]+)\s*;?\s*$')
        if len(lis) == 0
            " assemble static imports for removal
            let lis = matchlist(line, '\v^\s*import static\s+(\w+\.)+([a-zA-Z0-9_]+)\s*;?\s*$')
        endif
        if len(lis) > 0
            let s = lis[2]
            let searchPattern = '\v(//.*)@<!<' . s . '>'

            " start searching from the class definition
            call cursor(classStartLine, 1)
            let linefound = search(searchPattern, 'nW')

            if linefound == 0
                if a:remove
                    exec linenr . 'd _'
                    let linenr -= 1
                    let classStartLine -= 1
                else
                    call add(s:matches_so_far, matchadd('unusedimport', '^' . line . '$'))
                endif
            endif
        endif
    endwhile

    " set cursor back to initial position
    call cursor(startLine, startCol)
endfunction

function! s:ResetUnusedHighlights()
    for id in s:matches_so_far
        call matchdelete(id)
    endfor
    let s:matches_so_far = []
endfunction

function! s:Vimporter_JavaLib(word)
    let java_roots = copy(g:vimporter_java_roots)
    let add_roots = copy(g:vimporter_add_roots)
    let s:fuzzy_search = g:vimporter_fuzzy_search

    let totaldirs = s:GetDirList(java_roots + add_roots)
    " save current position to set it back later
    let s:startLine = line(".")
    let s:startCol = col(".")

    if empty(totaldirs)
        echo "No directories specified"
        return
    endif

    " Clear out the lookup dict
    let s:vimporter_global_lookup = {}

    echo "...Vimporter is searching"
    for doc in totaldirs
        let foundText = ""
        if match(doc, "\.jar") != -1
            let foundText = s:RetrieveFromJar(doc, a:word)
        else
            let foundText = s:RetrieveFromDir(doc, a:word)
        endif
        call s:SetDisplayNameInMap(foundText, doc)
    endfor

    if empty(s:vimporter_global_lookup)
        echo "Couldn't find: " . expand("<cword>")
    else
        " For Opening
        let s:vimporter_oldwindow = winnr()
        call writefile(s:DisplayList(), "__VimporterList__")
        botright 10split __VimporterList__
        call delete("__VimporterList__")

        if len(keys(s:vimporter_global_lookup)) == 1
            call VimporterImportJavaLine()
        else
            call s:InitializeBuffer()
        endif
    endif
endfunction

function! s:GetDirList(javalist)
    let locs = []
    for loca in a:javalist
        if match(loca, "\*\.jar") != -1
            let cmd = 'ls -1 ' . loca
            let tmplocs = split(system(cmd))
            for tmpl in tmplocs
                if match(tmpl ,"\.jar") != -1
                    call add(locs, tmpl)
                endif
            endfor
        else
            call add(locs, loca)
        endif
    endfor
    return locs
endfunction

function! s:InitializeBuffer()
    cal setpos(".", [0,0,0,0])
    setlocal nonu
    setlocal noro
    setlocal noswapfile
    setlocal nobuflisted
    setlocal buftype=nofile
    setlocal bufhidden=delete

    if v:version >= 700
        setlocal cursorline
    endif

    if has('syntax')
        syntax match vimportLine /^.*/
        highlight link vimportLine Directory
    endif

    nnoremap <buffer> <CR> :call VimporterImportJavaLine()<CR>
endfunction

function! s:RetrieveFromDir(doc, word)
    let cmd = ""

    if s:fuzzy_search == 1
        let cmd = 'find ' . a:doc . ' -name "*' . a:word . '*"'
    else
        let cmd = 'find ' . a:doc . ' -name ' . a:word . '.java'
    endif

    let foundtext = system(cmd)
    " let foundtext = substitute(foundtext, a:doc, "", "g")
    return foundtext
endfunction

function! s:RetrieveFromJar(doc, word)
    if s:fuzzy_search == 1
        let cmd = 'jar -tf ' . a:doc . ' | grep ' . a:word
    else
        let cmd = 'jar -tf ' . a:doc . ' | grep "/' . a:word . '\(\.class\|\.java\)"'
    endif

    let foundtext = system(cmd)
    return foundtext
endfunction

function! s:SetDisplayNameInMap(text, doc)
    let short_list = split(a:text, "\n")
    let short_list_long = copy(short_list)

    for i in range(len(short_list))
        let short_list[i] = s:TrimWhitespace(short_list[i])
        let short_list[i] = substitute(short_list[i], a:doc, '', '')
        let short_list[i] = substitute(short_list[i], '^/', '', '')
        let short_list[i] = substitute(short_list[i], 'src/main/java/', '', '')
        let short_list[i] = substitute(short_list[i], '\..*$', '', '')
        let short_list[i] = substitute(short_list[i], '\$[0-9]\+\s*$', '', '')
        let short_list[i] = substitute(short_list[i], '/\|\$', '.', 'g')
        let s:vimporter_global_lookup[short_list[i]] = short_list_long[i]
    endfor
endfunction

function! s:DisplayList()
    return sort(keys(s:vimporter_global_lookup))
endfunction

function! s:TrimWhitespace(input_string)
    let out = substitute(a:input_string, '^\s*', '', '')
    let out = substitute(a:input_string, '\s*$', '', '')
    return out
endfunction

function! VimporterImportJavaLine()
    let preport = substitute(getline("."), "\s*||\s*", "", "g")
    bd
    exe s:vimporter_oldwindow . " wincmd w"

    if search("^package .*") > 0
        s/^package .*/\=getline(".") . "\n\nimport " . preport . ";"/
        let s:startLine += 2
    elseif search("^import .*") > 0
        s/^import .*/\=getline(".") . "\nimport " . preport . ";"/
        let s:startLine += 1
    else
        call setpos(".", [0,0,0,0])
        s/^/\="import " . preport . ";\n"/
    endif

    " set cursor back to initial position
    call cursor(s:startLine, s:startCol)
endfunction

" From: http://vim.wikia.com/wiki/Unique_sorting
function! s:Uniquify(list)
    let dictionary = {}
    for i in a:list
        execute "let dictionary[ '" . i . "' ] = ''"
    endfor
    return keys( dictionary )
endfunction

function! s:LittlelotGenPackage()
    let curr_file_name = expand("%")
    let curr_file_name = substitute(curr_file_name, '.*/src/main/java/', '', '')

    " strpart + strridx 实现
    " let curr_class = strpart(curr_file_name, strridx(curr_file_name, '/') + 1, strlen(curr_file_name))
    " let s:curr_class = strpart(curr_class, 0, stridx(curr_class, '.'))
    " let s:curr_package = substitute(strpart(curr_file_name, 0, strridx(curr_file_name, "/")), "/", "\.", "g")

    " substitute 实现
    let curr_class = substitute(curr_file_name, '\..*$', '', '')
    let s:curr_class = substitute(curr_class, '\(.*/\)*\(.*\)$', '\2', '')

    let curr_package = substitute(curr_file_name, '\(/.*\)*\(/.*\)$', '\1', '')
    let s:curr_package = substitute(curr_package, '/', '.', 'g')
endfunction

function! s:appendContent(content)
    try
        call append(line('.'), split(a:content, '\n'))
        silent exec 'normal dd'
    catch /.*/
    endtry
    " wall
endfunction

function! s:LittlelotGenClass()
    let s:package_line = "package " . s:curr_package . ";\n\n"
    let s:author_line = "/**\n * @author littlelot\n */\n"
    let s:curr_class_type = "class"
    let s:import_line = ""
    let s:annotate_line = ""
    let s:extends_line = ""
    let s:implements_line = ""
    let s:fields_line = ""

    if match(s:curr_class, 'Service$') > 0 || match(s:curr_class, 'Mapper$') > 0
                \ || match(s:curr_class, 'Dao$') > 0 || match(s:curr_class, 'Repository$') > 0
        let s:curr_class_type = "interface"
    endif

    if match(s:curr_class, 'BeanMapper$') > 0
        let s:import_line = s:import_line . "import org.mapstruct.Mapper;\n\n"
        let s:annotate_line = s:annotate_line . "@Mapper(componentModel=\"spring\")\n"
    elseif match(s:curr_class, 'Mapper$') > 0 || match(s:curr_class, 'MybatisDao$') > 0
        let s:import_line = s:import_line . "import org.apache.ibatis.annotations.Mapper;\n\n"
        let s:annotate_line = s:annotate_line . "@Mapper\n"
    elseif match(s:curr_class, 'Dao$') > 0 || match(s:curr_class, 'Repository$') > 0
        if match(s:curr_class, "Repository") > 0
            let curr_domain = substitute(s:curr_class, 'Repository$', '', '')
        else
            let curr_domain = substitute(s:curr_class, 'Dao$', '', '')
        endif

        let s:import_line = s:import_line . "import org.springframework.data.jpa.repository.JpaRepository;\n"
                    \ . "import " . substitute(s:curr_package, 'dao$', 'domain', '') . "." . curr_domain . ";\n\n"
        let s:extends_line = s:extends_line . "extends JpaRepository<" . curr_domain . ",String> "
    elseif match(s:curr_class, 'ServiceImpl$') > 0
        let curr_service = substitute(s:curr_class, 'Impl$', '', '')
        let curr_domain = substitute(s:curr_class, 'ServiceImpl$', '', '')

        let s:import_line = s:import_line . "import org.springframework.stereotype.Service;\n"
                    \ . "import org.springframework.beans.factory.annotation.Autowired;\n"
                    \ . "import " . substitute(s:curr_package, '\.impl$', '', '') . "." . curr_service . ";\n"
        for key in keys(g:autowired_dict_s)
            let s:import_line = s:import_line . "import "
                        \ . substitute(s:curr_package, '\.service\.impl$', '', '')
                        \ . "." . key . "." . curr_domain . g:autowired_dict_s[key] . ";\n"
            let s:fields_line = s:fields_line . "\n\t@Autowired\n"
                        \ . "\tprivate " . curr_domain . g:autowired_dict_s[key] . " "
                        \ . substitute(curr_domain, '.*', '\l\0', '') . g:autowired_dict_s[key] . ";\n"
        endfor
        let s:import_line = s:import_line . "\n"

        let s:annotate_line = s:annotate_line . "@Service\n"
        let s:implements_line = s:implements_line . "implements " . curr_service . " "
    elseif match(s:curr_class, 'Controller$') > 0
        let curr_domain = substitute(s:curr_class, 'Controller$', '', '')

        let s:import_line = s:import_line . "import org.springframework.web.bind.annotation.RestController;\n"
                    \ . "import org.springframework.web.bind.annotation.RequestMapping;\n"
                    \ . "import org.springframework.beans.factory.annotation.Autowired;\n"
                    \ . "import io.swagger.annotations.Api;\n"
        for key in keys(g:autowired_dict_c)
            let s:import_line = s:import_line . "import "
                        \ . substitute(s:curr_package, '\.controller$', '', '')
                        \ . "." . key . "." . curr_domain . g:autowired_dict_c[key] . ";\n"
            let s:fields_line = s:fields_line . "\n\t@Autowired\n"
                        \ . "\tprivate " . curr_domain . g:autowired_dict_c[key] . " "
                        \ . substitute(curr_domain, '.*', '\l\0', '') . g:autowired_dict_c[key] . ";\n"
        endfor
        let s:import_line = s:import_line . "\n"

        let s:annotate_line = s:annotate_line . "@RestController\n"
                    \ . "@RequestMapping(\"/\")\n"
                    \ . "@Api(value=\"\",tags=\"\")\n"
    elseif match(s:curr_package, 'domain$') > 0
        let s:import_line = s:import_line . "import javax.persistence.Entity;\n"
                    \ . "import javax.persistence.Table;\n"
                    \ . "import io.swagger.annotations.ApiModel;\n"
                    \ . "import lombok.Data;\n\n"
        let s:annotate_line = s:annotate_line . "@Data\n@Entity\n@ApiModel(\"\")\n@Table(name=\"\")\n"
    elseif match(s:curr_package, 'vo$') > 0 || match(s:curr_package, 'pojo$') > 0
        let s:import_line = s:import_line . "import lombok.NoArgsConstructor;\n"
                    \ . "import lombok.Data;\n\n"
        let s:annotate_line = s:annotate_line . "@Data\n@NoArgsConstructor\n"
    endif

    let s:template_str = s:package_line . s:import_line . s:author_line . s:annotate_line
                \ . "public " . s:curr_class_type . " " . s:curr_class . " " . s:extends_line
                \ . s:implements_line . "{\n" . s:fields_line . "}"

    call s:appendContent(s:template_str)
endfunction

function! s:LittlelotAutoGen()
    let lastLineNo = line('$')
    if lastLineNo != 1 || len(getline(lastLineNo)) != 0
        return
    endif

    call s:LittlelotGenPackage()
    call s:LittlelotGenClass()
endfunction
